/*
    Open Manager For Tablets, an open source file manager for the Android system
    Copyright (C) 2011  Joe Berria <nexesdevelopment@gmail.com>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package vn.edu.uit.file_manager;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.Stack;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import net.lingala.zip4j.core.ZipFile;
import net.lingala.zip4j.exception.ZipException;
import net.lingala.zip4j.model.UnzipParameters;
import net.lingala.zip4j.model.ZipParameters;
import net.lingala.zip4j.util.Zip4jConstants;
import android.app.AlertDialog;
import android.content.Context;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;

/**
 * This class is completely modular, which is to say that it has no reference to
 * the any GUI activity. This class could be taken and placed into in other java
 * (not just Android) project and work. <br>
 * <br>
 * This class handles all file and folder operations on the system. This class
 * dictates how files and folders are copied/pasted, (un)zipped renamed and
 * searched. The EventHandler class will generally call these methods and have
 * them performed in a background thread. Threading is not done in this class.
 * 
 * @author Joe Berria
 * 
 */
public class FileManager {
	private static final int BUFFER = 2048;
	private static final int SORT_NONE = 0;
	private static final int SORT_ALPHA = 1;
	private static final int SORT_TYPE = 2;
	private static final int SORT_SIZE = 3;

	private static final int KB = 1024;
	private static final int MB = KB * KB;
	private static final int GB = MB * KB;

	private boolean mShowHiddenFiles = false;
	private int mSortType = SORT_ALPHA;
	private long mDirSize = 0;
	private long mFoldersOfDir = 0;
	private long mFilesOfDir = 0;
	private Stack<String> mPathStack;
	private ArrayList<String> mDirContent;

	/**
	 * Constructs an object of the class <br>
	 * this class uses a stack to handle the navigation of directories.
	 */
	public FileManager() {
		mDirContent = new ArrayList<String>();
		mPathStack = new Stack<String>();

		mPathStack.push("/");
		mPathStack.push(mPathStack.peek() + "sdcard");
	}

	/**
	 * This will return a string of the current directory path
	 * 
	 * @return the current directory
	 */
	public String getCurrentDir() {
		return mPathStack.peek();
	}

	/**
	 * This will return a string of the current home path.
	 * 
	 * @return the home directory
	 */
	public ArrayList<String> setHomeDir(String name) {
		// This will eventually be placed as a settings item
		mPathStack.clear();
		mPathStack.push("/");
		mPathStack.push(name);

		return populate_list();
	}

	/**
	 * This will determine if hidden files and folders will be visible to the
	 * user.
	 * 
	 * @param choice
	 *            true if user is veiwing hidden files, false otherwise
	 */
	public void setShowHiddenFiles(boolean choice) {
		mShowHiddenFiles = choice;
	}

	/**
	 * 
	 * @param type
	 */
	public void setSortType(int type) {
		mSortType = type;
	}

	/**
	 * This will return a string that represents the path of the previous path
	 * 
	 * @return returns the previous path
	 */
	public ArrayList<String> getPreviousDir() {
		int size = mPathStack.size();

		if (size >= 2)
			mPathStack.pop();

		else if (size == 0)
			mPathStack.push("/");

		return populate_list();
	}

	/**
	 * 
	 * @param path
	 * @param isFullPath
	 * @return
	 */
	public ArrayList<String> getNextDir(String path, boolean isFullPath) {
		int size = mPathStack.size();

		if (!path.equals(mPathStack.peek()) && !isFullPath) {
			if (size == 1)
				mPathStack.push("/" + path);
			else
				mPathStack.push(mPathStack.peek() + "/" + path);
		}

		else if (!path.equals(mPathStack.peek()) && isFullPath) {
			mPathStack.push(path);
		}

		return populate_list();
	}

	/**
	 * 
	 * @param old
	 *            the file to be copied
	 * @param newDir
	 *            the directory to move the file to
	 * @return
	 */
	public int copyToDirectory(String old, String newDir) {
		File old_file = new File(old);
		File temp_dir = new File(newDir);
		byte[] data = new byte[BUFFER];
		int read = 0;

		if (old_file.isFile() && temp_dir.isDirectory() && temp_dir.canWrite()) {
			String file_name = old
					.substring(old.lastIndexOf("/"), old.length());
			File cp_file = new File(newDir + file_name);

			try {
				BufferedOutputStream o_stream = new BufferedOutputStream(
						new FileOutputStream(cp_file));
				BufferedInputStream i_stream = new BufferedInputStream(
						new FileInputStream(old_file));

				while ((read = i_stream.read(data, 0, BUFFER)) != -1)
					o_stream.write(data, 0, read);

				o_stream.flush();
				i_stream.close();
				o_stream.close();

			} catch (FileNotFoundException e) {
				Log.e("FileNotFoundException", e.getMessage());
				return -1;

			} catch (IOException e) {
				Log.e("IOException", e.getMessage());
				return -1;
			}

		} else if (old_file.isDirectory() && temp_dir.isDirectory()
				&& temp_dir.canWrite()) {
			String files[] = old_file.list();
			String dir = newDir
					+ old.substring(old.lastIndexOf("/"), old.length());
			int len = files.length;

			if (!new File(dir).mkdir())
				return -1;

			for (int i = 0; i < len; i++)
				copyToDirectory(old + "/" + files[i], dir);

		} else if (!temp_dir.canWrite())
			return -1;

		return 0;
	}

	/**
	 * 
	 * @param zipName
	 * @param toDir
	 * @param fromDir
	 */
	public void extractZipFilesFromDir(String zipName, String toDir,
			String fromDir) {
		if (!(toDir.charAt(toDir.length() - 1) == '/'))
			toDir += "/";
		if (!(fromDir.charAt(fromDir.length() - 1) == '/'))
			fromDir += "/";

		String org_path = fromDir + zipName;

		//extractZipFiles(org_path, toDir);
	}

	/**
	 * 
	 * Giai nen cung thu muc
	 * 
	 * @param zip_file
	 * @param directory
	 */
	public void extractZipFiles(String zipFile, String directory, Context mContext) {			
		try {
			UnzipParameters unzipParam = new UnzipParameters();
			ZipFile file = new ZipFile(directory + zipFile);
			if (file.isEncrypted()) {
				file.setPassword("123456");				
				file.extractAll(directory, unzipParam);
			} else {
				file.extractAll(directory, unzipParam);
			}
		} catch (ZipException e) {
			// TODO Auto-generated catch block			
			e.printStackTrace();
			
			//Toast.makeText(mContext, "sai pass", Toast.LENGTH_SHORT).show();
			return;
		}
		
//		byte[] data = new byte[BUFFER];
//		String name, path, zipDir;
//		ZipEntry entry;
//		ZipInputStream zipstream;
//
//		if (!(directory.charAt(directory.length() - 1) == '/'))
//			directory += "/";
//
//		if (zip_file.contains("/")) {
//			path = zip_file;
//			name = path.substring(path.lastIndexOf("/") + 1, path.length() - 4);
//			zipDir = directory + name + "/";
//
//		} else {
//			path = directory + zip_file;
//			name = path.substring(path.lastIndexOf("/") + 1, path.length() - 4);
//			zipDir = directory + name + "/";
//		}
//
//		new File(zipDir).mkdir();
//
//		try {
//			zipstream = new ZipInputStream(new FileInputStream(path));
//
//			while ((entry = zipstream.getNextEntry()) != null) {
//				String buildDir = zipDir;
//				String[] dirs = entry.getName().split("/");
//
//				if (dirs != null && dirs.length > 0) {
//					for (int i = 0; i < dirs.length - 1; i++) {
//						buildDir += dirs[i] + "/";
//						new File(buildDir).mkdir();
//					}
//				}
//
//				int read = 0;
//				FileOutputStream out = new FileOutputStream(zipDir
//						+ entry.getName());
//				while ((read = zipstream.read(data, 0, BUFFER)) != -1)
//					out.write(data, 0, read);
//
//				zipstream.closeEntry();
//				out.close();
//			}
//
//		} catch (FileNotFoundException e) {
//			e.printStackTrace();
//
//		} catch (IOException e) {
//			e.printStackTrace();
//		}
	}

	/**
	 * 
	 * @param path
	 */
	public void createZipFile(String path, String name) {
		String _path;
		if (path.charAt(path.length() - 1) != '/')
			_path = path + "/";
		else
			_path = path;

		File dir = new File(_path + name);
		String[] list = dir.list();
		// String name = path.substring(path.lastIndexOf("/"), path.length());

		if (!dir.canRead() || !dir.canWrite())
			return;

		int len = list.length;

		try {
			ZipOutputStream zip_out = new ZipOutputStream(
					new BufferedOutputStream(new FileOutputStream(_path + name
							+ ".zip"), BUFFER));

			for (int i = 0; i < len; i++)
				zip_folder(new File(_path + name + "/" + list[i]), zip_out);

			zip_out.close();

		} catch (FileNotFoundException e) {
			Log.e("File not found", e.getMessage());

		} catch (IOException e) {
			Log.e("IOException", e.getMessage());
		}
	}

	/**
	 * 
	 * @param filePath
	 * @param newName
	 * @return
	 */
	public int renameTarget(String filePath, String newName) {
		File src = new File(filePath);
		String ext = "";
		File dest;

		if (src.isFile())
			/* get file extension */
			ext = filePath.substring(filePath.lastIndexOf("."),
					filePath.length());

		if (newName.length() < 1)
			return -1;

		String temp = filePath.substring(0, filePath.lastIndexOf("/"));

		dest = new File(temp + "/" + newName + ext);
		if (src.renameTo(dest))
			return 0;
		else
			return -1;
	}

	/**
	 * 
	 * @param path
	 * @param name
	 * @return
	 */
	public int createDir(String path, String name) {
		int len = path.length();

		if (len < 1 || len < 1)
			return -1;

		if (path.charAt(len - 1) != '/')
			path += "/";

		if (new File(path + name).mkdir())
			return 0;

		return -1;
	}

	/**
	 * 
	 * @param path
	 * @param name
	 * @return
	 * @throws IOException
	 */
	public int createFile(String path, String name) throws IOException {
		int len = path.length();

		if (len < 1 || len < 1)
			return -1;

		if (path.charAt(len - 1) != '/')
			path += "/";

		if (new File(path + name).createNewFile())
			return 0;

		return -1;
	}

	/**
	 * The full path name of the file to delete.
	 * 
	 * @param path
	 *            name
	 * @return
	 */
	public int deleteTarget(String path) {
		File target = new File(path);

		if (target.exists() && target.isFile() && target.canWrite()) {
			target.delete();
			return 0;
		}

		else if (target.exists() && target.isDirectory() && target.canRead()) {
			String[] file_list = target.list();

			if (file_list != null && file_list.length == 0) {
				target.delete();
				return 0;

			} else if (file_list != null && file_list.length > 0) {

				for (int i = 0; i < file_list.length; i++) {
					File temp_f = new File(target.getAbsolutePath() + "/"
							+ file_list[i]);

					if (temp_f.isDirectory())
						deleteTarget(temp_f.getAbsolutePath());
					else if (temp_f.isFile())
						temp_f.delete();
				}
			}
			if (target.exists())
				if (target.delete())
					return 0;
		}
		return -1;
	}

	/**
	 * 
	 * @param name
	 * @return
	 */
	public boolean isDirectory(String name) {
		return new File(mPathStack.peek() + "/" + name).isDirectory();
	}

	/**
	 * converts integer from wifi manager to an IP address.
	 * 
	 * @param des
	 * @return
	 */
	public static String integerToIPAddress(int ip) {
		String ascii_address = "";
		int[] num = new int[4];

		num[0] = (ip & 0xff000000) >> 24;
		num[1] = (ip & 0x00ff0000) >> 16;
		num[2] = (ip & 0x0000ff00) >> 8;
		num[3] = ip & 0x000000ff;

		ascii_address = num[0] + "." + num[1] + "." + num[2] + "." + num[3];

		return ascii_address;
	}

	/**
	 * 
	 * @param dir
	 * @param pathName
	 * @return
	 */
	public ArrayList<String> searchInDirectory(String dir, String pathName) {
		ArrayList<String> names = new ArrayList<String>();
		search_file(dir, pathName, names);

		return names;
	}

	/**
	 * 
	 * @param path
	 * @return
	 */
	public long getDirSize(String path) {
		get_dir_size(new File(path));

		return mDirSize;
	}

	private String convertSizeToString(long size) {
		String result = "";
		long _size = size;

		if (_size > GB)
			result = String.format("%.2f GB ", (double) _size / GB);
		else if (_size < GB && _size > MB)
			result = String.format("%.2f MB ", (double) _size / MB);
		else if (_size < MB && _size > KB)
			result = String.format("%.2f KB ", (double) _size / KB);
		else
			result = String.format("%.2f bytes ", (double) _size);

		return result;
	}

	/**
	 * 
	 * @param dir
	 *            thu muc chua item can xem thong tin
	 * @param nameItem
	 *            ten item can xem thong tin
	 * @return
	 */
	public ArrayList<String> getInfo(String dir, String nameItem) {
		ArrayList<String> info = new ArrayList<String>();
		mDirSize = 0;
		mFilesOfDir = 0;
		mFoldersOfDir = 0;
		File f = new File(dir + "/" + nameItem);
		if (f.exists() && f.canRead()) {
			info.add(nameItem);
			info.add(dir);
			if (f.isFile()) {
				info.add("File");
				info.add(convertSizeToString(f.length()));
			} else {
				info.add("Folder");
				long size = getDirSize(dir + "/" + nameItem);
				info.add(mFilesOfDir + " Files, " + mFoldersOfDir + " Folders");
				info.add(convertSizeToString(size));
			}

			info.add(new Date(f.lastModified()) + " ");

			if (f.canRead()) {
				info.add("Yes");
			} else {
				info.add("No");
			}

			if (f.canWrite()) {
				info.add("Yes");
			} else {
				info.add("No");
			}

			if (f.isHidden()) {
				info.add("Yes");
			} else {
				info.add("No");
			}
		}
		return info;
	}

	public ArrayList<String> getMultiInfo(String path, ArrayList<String> input) {
		ArrayList<String> multiInfo = new ArrayList<String>();
		long sumSize = 0;
		long sumFiles = 0;
		long sumFolders = 0;
		multiInfo.add("Multiple files");
		multiInfo.add(path);
		for (String str : input) {
			mDirSize = 0;
			mFilesOfDir = 0;
			mFoldersOfDir = 0;
			File f = new File(str);
			if (f.exists() && f.canRead()) {
				if (f.isFile()) {
					sumSize += f.length();
					sumFiles += 1;
				} else {
					sumSize += getDirSize(str);
					sumFiles += mFilesOfDir;
					sumFolders += 1 + mFoldersOfDir;
				}
			}
		}

		multiInfo.add(sumFiles + " Files, " + sumFolders + " Folders");
		multiInfo.add(convertSizeToString(sumSize));

		return multiInfo;
	}

	public void zipFile(String zipFile, String addedItem, boolean hasPassword,
			String password) {

		try {
			File f = new File(addedItem);
			ZipParameters parameters = new ZipParameters();
			parameters.setCompressionMethod(Zip4jConstants.COMP_DEFLATE);
			parameters.setCompressionLevel(Zip4jConstants.DEFLATE_LEVEL_ULTRA);

			if (hasPassword) {
				parameters.setEncryptFiles(true);
				parameters.setEncryptionMethod(Zip4jConstants.ENC_METHOD_AES);
				parameters.setAesKeyStrength(Zip4jConstants.AES_STRENGTH_256);
				parameters.setPassword(password);
			}
			ZipFile _ZipFile = new ZipFile(zipFile);

			if (f.isFile() && f.canRead()) {				
				_ZipFile.createZipFile(f, parameters);
			} else if (f.isDirectory() && f.canRead()) {
				_ZipFile.addFolder(f, parameters);
			}
			
			
		} catch (ZipException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}

	public void zipMultiFile(String zipFile, boolean hasPassword,
			String password, ArrayList<String> multiAddedItem) {
		try {

			ZipParameters parameters = new ZipParameters();
			parameters.setCompressionMethod(Zip4jConstants.COMP_DEFLATE);
			parameters.setCompressionLevel(Zip4jConstants.DEFLATE_LEVEL_ULTRA);

			if (hasPassword) {
				parameters.setEncryptFiles(true);
				parameters.setEncryptionMethod(Zip4jConstants.ENC_METHOD_AES);
				parameters.setAesKeyStrength(Zip4jConstants.AES_STRENGTH_256);
				parameters.setPassword(password);
			}

			ZipFile _ZipFile = new ZipFile(zipFile);
			
			File f;
			for (String addedItem : multiAddedItem) {
				f = new File(addedItem);
				if (f.isFile() && f.canRead()) {
					_ZipFile.addFile(f, parameters);
				} else if (f.isDirectory() && f.canRead()) {
					_ZipFile.addFolder(addedItem, parameters);
				}
			}

		} catch (ZipException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	private String displaySize(String pathItem) {
		String _displaySize = "";
		return _displaySize;
	}

	private static final Comparator alph = new Comparator<String>() {
		public int compare(String arg0, String arg1) {
			return arg0.toLowerCase().compareTo(arg1.toLowerCase());
		}
	};

	private final Comparator size = new Comparator<String>() {
		public int compare(String arg0, String arg1) {
			String dir = mPathStack.peek();
			Long first = new File(dir + "/" + arg0).length();
			Long second = new File(dir + "/" + arg1).length();

			return first.compareTo(second);
		}
	};

	private final Comparator type = new Comparator<String>() {
		public int compare(String arg0, String arg1) {
			String ext = null;
			String ext2 = null;
			int ret;

			try {
				ext = arg0.substring(arg0.lastIndexOf(".") + 1, arg0.length())
						.toLowerCase();
				ext2 = arg1.substring(arg1.lastIndexOf(".") + 1, arg1.length())
						.toLowerCase();

			} catch (IndexOutOfBoundsException e) {
				return 0;
			}
			ret = ext.compareTo(ext2);

			if (ret == 0)
				return arg0.toLowerCase().compareTo(arg1.toLowerCase());

			return ret;
		}
	};

	/*
	 * (non-Javadoc) this function will take the string from the top of the
	 * directory stack and list all files/folders that are in it and return that
	 * list so it can be displayed. Since this function is called every time we
	 * need to update the the list of files to be shown to the user, this is
	 * where we do our sorting (by type, alphabetical, etc).
	 * 
	 * @return
	 */
	private ArrayList<String> populate_list() {

		if (!mDirContent.isEmpty())
			mDirContent.clear();

		File file = new File(mPathStack.peek());

		if (file.exists() && file.canRead()) {
			String[] list = file.list();
			int len = list.length;

			/* add files/folder to arraylist depending on hidden status */
			for (int i = 0; i < len; i++) {
				if (!mShowHiddenFiles) { // khong hien thi item an nen ko them
											// vao arraylist
					if (list[i].toString().charAt(0) != '.')
						mDirContent.add(list[i]);

				} else {
					mDirContent.add(list[i]);
				}
			}

			/* sort the arraylist that was made from above for loop */
			switch (mSortType) {
			case SORT_NONE:
				// no sorting needed
				break;

			case SORT_ALPHA:
				Object[] tt = mDirContent.toArray();
				mDirContent.clear();

				Arrays.sort(tt, alph);

				for (Object a : tt) {
					mDirContent.add((String) a);
				}
				break;

			case SORT_SIZE:
				int index = 0;
				Object[] size_ar = mDirContent.toArray();
				String dir = mPathStack.peek();

				Arrays.sort(size_ar, size);

				mDirContent.clear();
				for (Object a : size_ar) {
					if (new File(dir + "/" + (String) a).isDirectory())
						mDirContent.add(index++, (String) a);
					else
						mDirContent.add((String) a);
				}
				break;

			case SORT_TYPE:
				int dirindex = 0;
				Object[] type_ar = mDirContent.toArray();
				String current = mPathStack.peek();

				Arrays.sort(type_ar, type);
				mDirContent.clear();

				for (Object a : type_ar) {
					if (new File(current + "/" + (String) a).isDirectory())
						mDirContent.add(dirindex++, (String) a);
					else
						mDirContent.add((String) a);
				}
				break;
			}

		} else {
			mDirContent.add("Emtpy");
		}

		return mDirContent;
	}

	/*
	 * 
	 * @param file
	 * 
	 * @param zout
	 * 
	 * @throws IOException
	 */
	private void zip_folder(File file, ZipOutputStream zout) throws IOException {
		byte[] data = new byte[BUFFER];
		int read;

		if (file.isFile()) {
			ZipEntry entry = new ZipEntry(file.getName());
			zout.putNextEntry(entry);
			BufferedInputStream instream = new BufferedInputStream(
					new FileInputStream(file));

			while ((read = instream.read(data, 0, BUFFER)) != -1)
				zout.write(data, 0, read);

			zout.closeEntry();
			instream.close();

		} else if (file.isDirectory()) {
			String[] list = file.list();
			int len = list.length;

			for (int i = 0; i < len; i++)
				zip_folder(new File(file.getPath() + "/" + list[i]), zout);
		}
	}

	/*
	 * 
	 * @param path
	 */
	private void get_dir_size(File path) {
		File[] list = path.listFiles();
		int len;

		if (list != null) {
			len = list.length;

			for (int i = 0; i < len; i++) {
				try {
					if (list[i].isFile() && list[i].canRead()) {
						mDirSize += list[i].length();
						mFilesOfDir += 1;
					} else if (list[i].isDirectory() && list[i].canRead()
							&& !isSymlink(list[i])) {
						mFoldersOfDir += 1;
						get_dir_size(list[i]);
					}
				} catch (IOException e) {
					Log.e("IOException", e.getMessage());
				}
			}
		}
	}

	// Inspired by org.apache.commons.io.FileUtils.isSymlink()
	private static boolean isSymlink(File file) throws IOException {
		File fileInCanonicalDir = null;
		if (file.getParent() == null) {
			fileInCanonicalDir = file;
		} else {
			File canonicalDir = file.getParentFile().getCanonicalFile();
			fileInCanonicalDir = new File(canonicalDir, file.getName());
		}
		return !fileInCanonicalDir.getCanonicalFile().equals(
				fileInCanonicalDir.getAbsoluteFile());
	}

	/*
	 * (non-JavaDoc) I dont like this method, it needs to be rewritten. Its
	 * hacky in that if you are searching in the root dir (/) then it is not
	 * going to be treated as a recursive method so the user dosen't have to sit
	 * forever and wait.
	 * 
	 * I will rewrite this ugly method.
	 * 
	 * @param dir directory to search in
	 * 
	 * @param fileName filename that is being searched for
	 * 
	 * @param n ArrayList to populate results
	 */
	private void search_file(String dir, String fileName, ArrayList<String> n) {
		File root_dir = new File(dir);
		String[] list = root_dir.list();

		if (list != null && root_dir.canRead()) {
			int len = list.length;

			for (int i = 0; i < len; i++) {
				File check = new File(dir + "/" + list[i]);
				String name = check.getName();

				if (check.isFile()
						&& name.toLowerCase().contains(fileName.toLowerCase())) {
					n.add(check.getPath());
				} else if (check.isDirectory()) {
					if (name.toLowerCase().contains(fileName.toLowerCase()))
						n.add(check.getPath());

					else if (check.canRead() && !dir.equals("/"))
						search_file(check.getAbsolutePath(), fileName, n);
				}
			}
		}
	}
}
